//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using EffiProzDB.Lib;

namespace EffiProzDB
{

    /**
     * Handles initial attempts to connect to HSQLDB databases within the JVM
     * (or a classloader within the JVM). Opens the database if it is not open
     * or connects to it if it is already open. This allows the same database to
     * be used by different instances of Server and by direct connections.<p>
     *
     * Maintains a map of Server instances and notifies each server when its
     * database has shut down.<p>
     *
     * Maintains a reference to the timer used for file locks and logging.<p>
     *
     * @author fredt@users
     * @version 1.8.0
     * @since 1.7.2
     */
    public class DatabaseManager
    {

        // Database and Server registry

        /** provides unique ID's for the Databases currently _in registry */
        private static int dbIDCounter;

        /** name to Database mapping for mem: databases */
        public static Dictionary<string, Database> memDatabaseMap = new Dictionary<string, Database>();

        /** File to Database mapping for file: databases */
        public static Dictionary<string, Database> fileDatabaseMap = new Dictionary<string, Database>();
      

        /** id number to Database for Databases currently _in registry */
        public static Dictionary<int, Database> databaseIDMap = new Dictionary<int, Database>();

        /**
         * Returns a vector containing the URI (type + path) for all the databases.
         */
        public static List<string> getDatabaseURIs()
        {

            List<string> v = new List<string>();         

            foreach (Database db in databaseIDMap.Values)
            {
                v.Add(db.getURI());
            }

            return v;
        }

        /**
         * Closes all the databases using the given mode.<p>
         *
         * CLOSEMODE_IMMEDIATELY = -1;
         * CLOSEMODE_NORMAL      = 0;
         * CLOSEMODE_COMPACT     = 1;
         * CLOSEMODE_SCRIPT      = 2;
         */
        public static void closeDatabases(int mode)
        {
            foreach (Database db in databaseIDMap.Values)
            {               
                try
                {
                    db.close(mode);
                }
                catch (HsqlException) { }
            }       
        }

        /**
         * Used by server to open a new session
         */
        public static Session newSession(int dbID, string user,
                                  string password)
        {

            Database db = (Database)databaseIDMap[dbID];

            return db == null ? null
                              : db.connect(user, password);
        }

            /**
             * Used by in-process connections and by Servlet
             */

        // loosecannon1@users 1.7.2 patch properties on the JDBC URL
            public static Session newSession(string type, string path, string user,
                                             string password,
                                             HsqlProperties props)
                                               {

                Database db = getDatabase(type, path, props);

                return db == null ? null
                                  : db.connect(user, password);
            }

            /**
             * Returns an existing session. Used with repeat HTTP connections
             * belonging to the same JDBC Conenction / HSQL Session pair.
             */
            public static Session getSession(int dbId, int sessionId) {

                Database db = null;
               databaseIDMap.TryGetValue(dbId, out db);

                return db == null ? null
                                  : db.sessionManager.getSession(sessionId);
            }

            /**
             * Used by server to open or create a database
             */

        // loosecannon1@users 1.7.2 patch properties on the JDBC URL
            public static int getDatabase(string type, string path, Server server,
                                   HsqlProperties props)
            {

                Database db = getDatabase(type, path, props);

                registerServer(server, db);

                return db.databaseID;
            } 

            /**
             * This has to be improved once a threading model is _in place.
             * Current behaviour:
             *
             * Attempts to connect to different databases do not block. Two db's can
             * open simultaneously.
             *
             * Attempts to connect to a db while it is opening or closing will block
             * until the db is open or closed. At this point the db state is either
             * DATABASE_ONLINE (after db.open() has returned) which allows a new
             * connection to be made, or the state is DATABASE_SHUTDOWN which means
             * the db can be reopened for the new connection).
             *
             */

        // loosecannon1@users 1.7.2 patch properties on the JDBC URL
            public static Database getDatabase(string type, string path,
                                        HsqlProperties props)   {

                // If the (type, path) pair does not correspond to a registered
                // instance, then getDatabaseObject() returns a newly constructed
                // and registered Database instance.
                // The database state will be DATABASE_SHUTDOWN,
                // which means that the switch below will attempt to
                // open the database instance.
                Database db = getDatabaseObject(type, path, props);

                lock (db) {
                    switch (db.getState()) {

                        case Database.DATABASE_ONLINE :
                            break;

                        case Database.DATABASE_SHUTDOWN :

                            // if the database was shutdown while this attempt
                            // was waiting, add the database back to the registry
                            if (lookupDatabaseObject(type, path) == null) {
                                addDatabaseObject(type, path, db);
                            }

                            db.open();
                            break;

                        // This state will currently not be reached as Database.Close() is
                        // called while a lock is held on the database.
                        // If we remove the lock from this method and a database is
                        // being shutdown by a thread and _in the meantime another thread
                        // attempts to connect to the db. The threads could belong to
                        // different server instances or be in-process.
                        case Database.DATABASE_CLOSING :

                        // this case will not be reached as the state is set and
                        // cleared within the db.open() call above, which is called
                        // from this synchronized block
                        // it is here simply as a placeholder for future development
                        case Database.DATABASE_OPENING :
                            throw Trace.error(Trace.LOCK_FILE_ACQUISITION_FAILURE,
                                              Trace.DatabaseManager_getDatabase);
                    }
                }

                return db;
            }

        // loosecannon1@users 1.7.2 patch properties on the JDBC URL
            private static  Database getDatabaseObject(string type,
                    string path, HsqlProperties props)   {

                        lock ( typeof(DatabaseManager))
                        {
                            Database db;
                            string key = path;
                            Dictionary<string, Database>  databaseMap;

                            if (type == DatabaseURL.S_FILE)
                            {
                                databaseMap = fileDatabaseMap;
                                key = filePathToKey(path);
                            }
                           
                            else if (type == DatabaseURL.S_MEM)
                            {
                                databaseMap = memDatabaseMap;
                            }
                            else
                            {
                                throw Trace.runtimeError(Trace.UNSUPPORTED_INTERNAL_OPERATION,
                                                         "DatabaseManager.getDatabaseObject");
                            }

                            db = null;
                     
                            if (!databaseMap.TryGetValue(key, out db))
                            {
                                db = new Database(type, path, type + key, props);
                                db.databaseID = dbIDCounter;

                                databaseIDMap.Add(dbIDCounter, db);

                                dbIDCounter++;

                                databaseMap.Add(key, db);
                            }

                            return db;
                        }
            }

            /**
             * Looks up database of a given type and path _in the registry. Returns
             * null if there is none.
             */
            private static  Database lookupDatabaseObject(string type,
                    string path)   {

                        lock (typeof(DatabaseManager))
                        {
                            string key = path;
                            Dictionary<string, Database> databaseMap;

                            if (type == DatabaseURL.S_FILE)
                            {
                                databaseMap = fileDatabaseMap;
                                key = filePathToKey(path);
                            }
                            //else if (type == DatabaseURL.S_RES)
                            //{
                            //    databaseMap = resDatabaseMap;
                            //}
                            else if (type == DatabaseURL.S_MEM)
                            {
                                databaseMap = memDatabaseMap;
                            }
                            else
                            {
                                throw (Trace.runtimeError(
                                    Trace.UNSUPPORTED_INTERNAL_OPERATION,
                                    "DatabaseManager.lookupDatabaseObject()"));
                            }

                            return (Database)databaseMap[key];
                        }
            }

            /**
             * Adds a database to the registry. Returns
             * null if there is none.
             */
            private static  void addDatabaseObject(string type,
                    string path, Database db)   {
                        lock (typeof(DatabaseManager))
                        {

                            string key = path;
                            Dictionary<string, Database> databaseMap;

                            if (type == DatabaseURL.S_FILE)
                            {
                                databaseMap = fileDatabaseMap;
                                key = filePathToKey(path);
                            }
                            //else if (type == DatabaseURL.S_RES)
                            //{
                            //    databaseMap = resDatabaseMap;
                            //}
                            else if (type == DatabaseURL.S_MEM)
                            {
                                databaseMap = memDatabaseMap;
                            }
                            else
                            {
                                throw Trace.runtimeError(Trace.UNSUPPORTED_INTERNAL_OPERATION,
                                                         "DatabaseManager.addDatabaseObject()");
                            }

                            databaseIDMap.Add(db.databaseID, db);
                            databaseMap.Add(key, db);
                        }
            }

            /**
             * Removes the database from registry.
             */
            public static void removeDatabase(Database database) {

                int     dbID = database.databaseID;
                string  type = database.getType();
                string  path = database.getPath();
                string  key  = path;
                Dictionary<string, Database> databaseMap;

                //notifyServers(database); TODOx

                if (type == DatabaseURL.S_FILE) {
                    databaseMap = fileDatabaseMap;

        // boucherb@users 20040124 - patch 1.7.2
        // Under the current contract, it's essentially impossible for an
        // exception to get thrown here, because the database could not
        // have been registered successfully before hand using the same
        // path
        //
        // Eventually, we might think about storing the key with the
        // database instance so as to avoid this unnecessary additional
        // conversion and highly unlikely corner case handling.
                    try {
                        key = filePathToKey(path);
                    } catch (HsqlException ) {
                       
                        string   foundKey = null;

                        foreach (var currentKey in databaseMap.Keys) {
                           
                            if (databaseMap[currentKey] == database) {
                                foundKey = currentKey;

                                break;
                            }
                        }

                        if (foundKey == null) {

                            // ??? return;
                        } else {
                            key = foundKey;
                        }
                    }
                } 
                //else if (type == DatabaseURL.S_RES) {
                //    databaseMap = resDatabaseMap;
                //} 
                else if (type == DatabaseURL.S_MEM) {
                    databaseMap = memDatabaseMap;
                } else {
                    throw (Trace.runtimeError(
                        Trace.UNSUPPORTED_INTERNAL_OPERATION,
                        "DatabaseManager.lookupDatabaseObject()"));
                }

                databaseIDMap.Remove(dbID);
                databaseMap.Remove(key);
               
            }

            /**
             * Maintains a map of servers to sets of databases.
             * Servers register each of their databases.
             * When a database is shutdown, all the servers accessing it are notified.
             * The database is then removed form the sets for all servers and the
             * servers that have no other database are removed from the map.
             */
            static Dictionary<Server, HashSet<Database>> serverMap = new Dictionary<Server, HashSet<Database>>();

            /**
             * Deregisters a server completely.
             */
            public static void deRegisterServer(Server server)
            {
                serverMap.Remove(server);
            }

            /**
             * Deregisters a server as serving a given database. Not yet used.
             */
            private static void deRegisterServer(Server server, Database db)
            {
              
                foreach (var databases in serverMap.Values)
                {
                    databases.Remove(db);

                    if (databases.Count == 0)
                    {
                        serverMap.Remove(server);
                    }
                }
            }

            /**
             * Registers a server as serving a given database.
             */
            private static void registerServer(Server server, Database db)
            {

                if (!serverMap.ContainsKey(server))
                {
                    serverMap.Add(server, new HashSet<Database>());
                }

                HashSet<Database> databases = (HashSet<Database>)serverMap[server];

                databases.Add(db);
            }

            /**
             * Notifies all servers that serve the database that the database has been
             * shutdown.
             */
            private static void notifyServers(Database db)
            {


                foreach (var server in serverMap.Keys)
                {

                    HashSet<Database> databases = (HashSet<Database>)serverMap[server];

                    if (databases.Contains(db))
                    {
                        server.notify(ServerConstants.SC_DATABASE_SHUTDOWN,
                                      db.databaseID);
                    }
                }
            }

            public static bool isServerDB(Database db)
            {
                foreach (var databases in serverMap.Values)
                {        
                    if (databases.Contains(db))
                    {
                        return true;
                    }
                }

                return false;
            }

        //// Timer
        //private static const HsqlTimer timer = new HsqlTimer();

        //public static HsqlTimer getTimer()
        //{
        //    return timer;
        //}

        // converts file path to database lookup key, converting any
        // thrown exception to an HsqlException _in the process
        private static string filePathToKey(string path)   {

            try {
                return FileUtil.GetDefaultInstance().CanonicalOrAbsolutePath(path);
            } catch (Exception e) {
                throw Trace.error(Trace.FILE_IO_ERROR, e.ToString());
            }
        }
    }
}
